/**
 * @fileoverview
 * JavaScript Object Template.
 * @author Hedger Wang (hedgerwang@gmail.com)
 */

/**
 * @constructor
 */
function Jot(arg_contents) {
  this.contents_ = [];
  this.childLogicBlockTypes_ = [];
  this.children_ = [];

  for (var i = 0, j = arguments.length; i < j; i++) {
    this.add(arguments[i]);
  }
}

// Constants ///////////////////////////////////////////////////////////////////

/**
 * @private
 * @param type
 */
Jot.MODEL_VALUE_PATTERN_ = /@{0,1}\{([$a-zA-z_]+[a-zA-z_0-9-]*)\}/ig;


/**
 * @type {string}
 * @private
 */
Jot.VALUE_ESCAPE_PREFIX_ = '@{';


/**
 * @type {string}
 */
Jot.ERROR_UNKNOW_RENDERER = 'Unknown renderer';

/**
 * @type {string}
 */
Jot.ERROR_PARENT_BLOCK_NULL = 'Parent Block is null';

/**
 * @type {string}
 */
Jot.ERROR_UNABLE_TO_CLOSE_LOGIC_BLOCK = 'Unable to close a logic block';

/**
 * @private
 * @type {Function}
 */
Jot.DUMMY_FALSE_FUNCTION = function() {
  return false;
};

/**
 * @private
 * @type {Function}
 */
Jot.DUMMY_TRUE_FUNCTION = function() {
  return true;
};


/**
 * @static
 * @private
 * @type {Object}
 */
Jot.LogicBlockType_ = {
  NOT:'not',
  WHEN:'when',
  EACH:'each',
  ELSE_WHEN:'elseWhen',
  OTHERWISE:'otherwise'
};

/**
 * @private
 * @type {Object}
 */
Jot.ModelKeys_ = {
  INDEX: '$index',
  KEY:'$key',
  VALUE:'$value',
  FIRST:'$first',
  LAST:'$last'
};

/**
 * @static
 */
(function() {
  var key,value;
  for (key in Jot.ModelKeys_) {
    value = Jot.ModelKeys_[key];
    Jot.ModelKeys_[value] = key;
  }
}());


/**
 * @type {Array}
 * @private
 */
Jot.prototype.contents_ = null;

/**
 * @private
 * @type {Jot.ModelKeys_}
 */
Jot.prototype.logicBlockType_ = null;

/**
 * @type {Array.<Jot.LogicBlockType_>}
 * @private
 */
Jot.prototype.childLogicBlockTypes_ = null;

/**
 * @type {Object}
 * @private
 */
Jot.prototype.model_ = null;

/**
 * @private
 * @type {Jot}
 */
Jot.prototype.parent_ = null;

/**
 * @private
 * @type {Jot}
 */
Jot.prototype.previous_ = null;


/**
 * @private
 * @type {Array.<Jot>}
 */
Jot.prototype.children_ = null;


/**
 * @private
 * @param {Jot.LogicBlockType_} args_type
 */
Jot.prototype.maybeEndLogicBlock_ = function(args_type) {
  if (!this.checkLogicBlock_.apply(this, arguments)) {
    throw new Error(Jot.ERROR_UNABLE_TO_CLOSE_LOGIC_BLOCK + '\n' +
                    Array.prototype.slice.call(arguments, 0).join(''));
  }
  this.parent_.childLogicBlockTypes_.pop();
};


/**
 * @private
 * @param type
 * @return {boolean}
 */
Jot.prototype.checkLogicBlock_ = function(args_type) {
  var parent = this.parent_;
  if (!parent) {
    throw new Error(Jot.ERROR_PARENT_BLOCK_NULL);
  }

  var currentType = parent.peekCurrentLogicBlock_();

  if (!currentType) {
    return false;
  }

  var i = 0,j = arguments.length;
  while (i < j) {
    if (arguments[i] == currentType) {
      return true;
    }
    i++;
  }
  return false;
};

/**
 * @private
 * @return {Jot.LogicBlockType_}
 */
Jot.prototype.peekCurrentLogicBlock_ = function() {
  if (this.childLogicBlockTypes_.length > 0) {
    return this.childLogicBlockTypes_[this.childLogicBlockTypes_.length - 1];
  } else {
    return null;
  }
};

/**
 * @private
 * @param template
 * @param renderer
 * @param logicType
 */
Jot.prototype.setUpChildTemplate_ = function(parent, template, renderer,
                                             logicType) {
  parent.childLogicBlockTypes_.push(logicType);
  parent.contents_.push(renderer);
  parent.children_.push(template);

  template.logicBlockType_ = logicType;
  template.parent_ = parent;

  if (logicType == Jot.LogicBlockType_.ELSE_WHEN ||
      logicType == Jot.LogicBlockType_.OTHERWISE) {
    template.previous_ = this;
  }

  return template;
};

/**
 * @private
 * @return {Object}
 */
Jot.prototype.getModel_ = function(opt_modelRef) {
  switch (Jot.typeOf(opt_modelRef)) {
    case 'number':
      return opt_modelRef;

    case 'function':;
      return opt_modelRef();

    case 'object':;
      return opt_modelRef;

    case 'string' :;
      break;

    default:
      opt_modelRef = '';
      break;
  }
  var template = this;

  while (!template.model_ && template.parent_) {
    template = template.parent_;
  }

  var model = template.model_;

  model = Jot.typeOf(model) == 'object' ? model : {};

  var names = opt_modelRef && opt_modelRef.split('.') || '';

  while (names.length) {
    model = model[names.shift()];
  }

  return model || {};
};

/**
 * @public
 * @param data
 */
Jot.prototype.add = function(data) {

  switch (Jot.typeOf(data)) {
    case 'string':
      if (!data) {
        break;
      }
      else if (data.indexOf('{') < 0 && data.indexOf('}') < 0) {
        // No variables found to be replaced.
        this.contents_.push(data);
      } else {
        this.contents_.push(new Jot.StringRenderer_(this, data));
      }

      break;

    case 'number':
      this.contents_.push(data);
      break;

    case 'object':
      this.contents_.push(data);
      break;

    default:
      break;
  }

  return this;
};

/**
 * A shorthand for the {#add} Method.
 * @public
 * @type {Function}
 */
Jot.prototype.a = Jot.prototype.add;

/**
 * @public
 * @param {string|Function} condition
 */
Jot.prototype.when = function(condition) {
  var template = new Jot();
  var renderer = new Jot.BooleanRenderer_(template, condition, true);
  this.setUpChildTemplate_(this, template, renderer, Jot.LogicBlockType_.WHEN);
  return template;
};

/**
 * @public
 */
Jot.prototype.endWhen = function() {
  this.maybeEndLogicBlock_(
      Jot.LogicBlockType_.WHEN,
      Jot.LogicBlockType_.ELSE_WHEN,
      Jot.LogicBlockType_.OTHERWISE);
  return this.parent_;
};


/**
 * @public
 * @param {string|Function} condition
 */
Jot.prototype.elseWhen = function(condition) {
  if (this.checkLogicBlock_(
      Jot.LogicBlockType_.WHEN,
      Jot.LogicBlockType_.ELSE_WHEN,
      Jot.LogicBlockType_.OTHERWISE)) {

    var template = new Jot();
    var renderer = new Jot.BooleanRenderer_(template, condition, true);
    this.setUpChildTemplate_(this.parent_, template, renderer,
        Jot.LogicBlockType_.WHEN);
  }
  return template;
};

/**
 * @public
 * @param {string|Function} condition
 */
Jot.prototype.otherwise = function() {
  if (this.checkLogicBlock_(
      Jot.LogicBlockType_.WHEN,
      Jot.LogicBlockType_.ELSE_WHEN)) {

    var template = new Jot();
    var renderer = new Jot.BooleanRenderer_(template, null, true);
    this.setUpChildTemplate_(this.parent_, template, renderer,
        Jot.LogicBlockType_.OTHERWISE);

    return template;
  } else {
    return null;
  }
};

/**
 * @public
 * @type {Function}
 */
Jot.prototype.other = Jot.prototype.otherwise;


/**
 * @public
 * @param condition
 */
Jot.prototype.not = function(condition) {
  var template = new Jot();
  var renderer = new Jot.BooleanRenderer_(template, condition, false);
  this.setUpChildTemplate_(this, template, renderer, Jot.LogicBlockType_.NOT);
  return template;
};

/**
 * @public
 */
Jot.prototype.endNot = function() {
  this.maybeEndLogicBlock_(Jot.LogicBlockType_.NOT);
  return this.parent_;
};


/**
 * @public
 * @param name
 */
Jot.prototype.each = function(iteratable) {
  this.childLogicBlockTypes_.push(Jot.LogicBlockType_.EACH);

  var template = new Jot();
  var renderer = new Jot.EachRenderer_(template, iteratable);
  this.setUpChildTemplate_(this, template, renderer, Jot.LogicBlockType_.EACH);
  return template;
};

/**
 * @public
 */
Jot.prototype.endEach = function() {
  this.maybeEndLogicBlock_(Jot.LogicBlockType_.EACH);
  return this.parent_;
};

/**
 * @public
 * @param model
 */
Jot.prototype.toString = function(model) {
  this.model_ = model;
  var output = this.contents_.join('');
  this.model_ = null;
  return output;
};


// Helper //////////////////////////////////////////////////////////////////////

/**
 * @param {string} str
 * @return {string}
 */
Jot.htmlEscape = function(str) {
  return str.
      replace(/&/g, '&amp;').
      replace(/>/g, '&gt;').
      replace(/</g, '&lt;').
      replace(/"/g, '&quot;');
};

/**
 * @private
 * @param v
 */
Jot.typeOf = function(v) {
  var t = typeof v;
  switch (t) {
    case 'undefined':
      return 'null';

    case 'object':
      if (!v) {
        return 'null';
      }
      return 'object';

    default:
      return t;
  }
};

/**
 * @return {boolean}
 */
Jot.isArray = function(obj) {
  return Object.prototype.toString.call(obj).toLowerCase().indexOf('array') > 0;
};


/**
 * @private
 * @type {Object}
 */
Jot.compiledFunctions_ = {};

/**
 * @private
 * @type {Function}
 */
Jot.compiledFunctionReplacer_ = function(m0, m1) {
  console.log(arguments);
  return ['model.' , m1].join('');
};

/**
 * @private
 * @param {string} condition
 */
Jot.createConditionFunction_ = function(condition) {
  var fn ;

  switch (Jot.typeOf(condition)) {
    case 'string':
      if (fn = Jot.compiledFunctions_[condition]) {
        return fn;
      }
      Jot.compiledFunctions_[condition] = fn = new Function('model',
          ['return (',
            condition.replace(
                Jot.MODEL_VALUE_PATTERN_,
                Jot.compiledFunctionReplacer_),
            ');'].join(''));

      return fn;

    case 'function':
      return condition;

    default:
      return condition ? Jot.DUMMY_TRUE_FUNCTION : Jot.DUMMY_FALSE_FUNCTION;
  }
};

// Base Renderers //////////////////////////////////////////////////////////////

Jot.BaseRender_ = function(template, data) {
  /**
   * @private
   * @type {Jot}
   */
  this.template_ = template;

  /**
   * @private
   * @type {string|number|Function}
   */
  this.data_ = data;
};

// String Renderers ////////////////////////////////////////////////////////////

/**
 * @constructor
 * @param {Jot} template
 * @param {string} data
 */
Jot.StringRenderer_ = function(template, string) {
  Jot.BaseRender_.call(this, template, string);
};

/**
 * @protected
 */
Jot.StringRenderer_.prototype.toString = function() {
  var template = this.template_;
  var model = template.getModel_();
  var data = this.data_;
  var reg = Jot.MODEL_VALUE_PATTERN_;
  var replacer;
  if (model && data && reg.test(data)) {
    replacer = function(m0, m1) {
      var escaped = ( m0.indexOf(Jot.VALUE_ESCAPE_PREFIX_) == 0 );       
      var value = model[m1];
      if (!value) {
        if (value === 0) {
          return 0;
        }
        if (value === '') {
          return '';
        }
        return m0; // Value not found, return the template key for debugging.
      }
      return escaped ? Jot.htmlEscape(value) : value;
    };
    data = data.replace(reg, replacer);
  }
  return data;
};

// Boolean Renderers ///////////////////////////////////////////////////////////

/**
 * @abstract
 * @constructor
 * @param template
 * @param condition
 */
Jot.BooleanRenderer_ = function(template, condition, boolTestValue_) {
  Jot.BaseRender_.call(this, template, condition);
  this.booleanValue_ = boolTestValue_;
};

/**
 * @private
 * @type {boolean}
 */
Jot.BooleanRenderer_.prototype.booleanValue_ = false;

/**
 * @private
 * @param condition
 * @param model
 */
Jot.BooleanRenderer_.prototype.ifTrue_ = function(condition, model) {
  var fn = Jot.createConditionFunction_(condition, model);
  return fn(model);
};


/**
 * @override
 * @public
 */
Jot.BooleanRenderer_.prototype.toString = function() {
  var condition = this.data_;
  var template = this.template_;
  var model = template.getModel_();

  switch (template.logicBlockType_) {
    case Jot.LogicBlockType_.ELSE_WHEN:;
      if (template.previous_.boolTestResult_) {
        // Previous WHEN Block already
        template.boolTestResult_ = true;
        return '';
      }
      break;

    case Jot.LogicBlockType_.OTHERWISE:;
      if (template.previous_.boolTestResult_) {
        // Previous WHEN Block already
        template.boolTestResult_ = true;
        return '';
      } else {
        return template.toString(model);
      }

    default:;
      break;
  }

  template.boolTestResult_ = this.ifTrue_(condition, model);
  if (template.boolTestResult_ == this.booleanValue_) {
    return template.toString(model);
  } else {
    return '';
  }
};


// Each Renderers //////////////////////////////////////////////////////////////

/**
 * @constructor
 * @param {Jot} template
 * @param {Function} data
 */
Jot.EachRenderer_ = function(template, iteratable) {
  Jot.BaseRender_.call(this, template, iteratable);
};


/**
 * @override
 * @public
 */
Jot.EachRenderer_.prototype.toString = function() {
  var template = this.template_;
  var iteratable = this.data_;
  var model = template.getModel_(iteratable);
  var output = [];
  var i,j,k, obj, value;
  var Jot_ModelKeys_ = Jot.ModelKeys_;

  switch (Jot.typeOf(model)) {
    case 'number':;
      i = 0;
      while (i < model) {
        obj = {};
        obj[Jot.ModelKeys_.INDEX] = i;
        output.push(template.toString(obj));
        i++;
      }
      break;

    case 'string':;
      alert('Not Implemented Yet :-P');
      break;

    case 'object':;

      if (model) {
        if (Jot.isArray(model)) {
          // Array Like
          i = 0;
          j = model.length;
          k = j - 1;
          while (i < j) {
            value = model[i];
            obj = Object(value);
            obj[Jot_ModelKeys_.INDEX] = i;
            obj[Jot_ModelKeys_.KEY] = i;
            obj[Jot_ModelKeys_.VALUE] = value;
            obj[Jot_ModelKeys_.FIRST] = (i == 0);
            obj[Jot_ModelKeys_.LAST] = (i == k);
            output.push(template.toString(obj));
            i++;
          }

          obj = null;
        } else {

          i = 0;
          for (j in model) {

            if (!Jot_ModelKeys_[j]) {
              obj = {};
              obj[Jot_ModelKeys_.INDEX] = i;
              obj[Jot_ModelKeys_.KEY] = j;
              obj[Jot_ModelKeys_.VALUE] = model[j];
              output.push(template.toString(obj));
              obj = null;
            }
            i++;
          }
        }
      }
      break;
    default:;
      break;
  }
  return output.join('');
};

